Embed Server
The embed server auto-deploys on every push to main. The widget is then available to any site at https://webreader.abair.ie/script.js.
Pipeline
| Stage | Trigger | What happens |
|---|---|---|
| CI | PR to main | Runs the build to verify the bundle still compiles. |
| CD | Merge to main | Builds a Docker image, pushes to the internal registry, deploys via SSH. |
| Live | — | https://webreader.abair.ie |
Cache lag after a deploy is minutes — the server emits must-revalidate so browsers and Cloudflare check back quickly.
If a deploy doesn't appear
- Check the Actions tab on GitHub for a failed workflow.
- SSH to the host and run
docker ps— the container'sCREATEDtimestamp tells you whether the new image actually started. - Most likely cause: the commit never got pushed. CD only sees
origin/main; local edits picked up bynpm run devaren't in the deployed bundle.
Rolling back
Revert the bad commit on main and push. CD rebuilds and redeploys. The widget URL stays the same — browsers pick up the rollback within minutes.
Embedding the production widget on a site
Once deployed, any site can load the widget with a single <script> tag.
One-line install
<script src="https://webreader.abair.ie/script.js" async></script>
Configuration
Set window.WebReaderConfig before the script loads. The boot script reads it once on init.
<script>
window.WebReaderConfig = {
locale: 'ga', // 'ga' (Irish) or 'en' (English)
panelPosition: { top: 80, left: 16 } // initial panel position in pixels
};
</script>
<script src="https://webreader.abair.ie/script.js" async></script>
| Option | Type | Default | Description |
|---|---|---|---|
locale | 'ga' | 'en' | 'ga' | UI language of the floating panel |
panelPosition | { top, left, right, bottom } (px) | { bottom: 16, right: 16 } | Starting position; user can drag from here |
React / Next.js (the canonical ABAIR pattern)
Load from a client component so it skips SSR. Re-mounts on locale change.
"use client";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
export default function Webreader() {
const { i18n } = useTranslation();
useEffect(() => {
const scriptId = "webreader-cdn-script";
if (document.getElementById(scriptId)) return;
const baseUrl =
process.env.NEXT_PUBLIC_WEBREADER_URL || "https://webreader.abair.ie";
(window as any).WebReaderConfig = {
locale: i18n.language === "ga-IE" ? "ga" : "en",
panelPosition: { top: 80, left: 16 },
};
const script = document.createElement("script");
script.id = scriptId;
script.src = `${baseUrl}/script.js`;
script.async = true;
document.body.appendChild(script);
}, [i18n.language]);
return null;
}
Mount <Webreader /> once in the root layout. The scriptId guard prevents double-mounting from route changes, hot reloads, or React StrictMode.
Environment switching
Use a public env var so the URL is available in the browser bundle:
# .env.local
NEXT_PUBLIC_WEBREADER_URL=http://localhost:3010
Leave it unset in production — the component falls back to https://webreader.abair.ie. Restart npm run dev after changing it. Next.js bakes NEXT_PUBLIC_* vars at server start.
Marking up the host page
Widget behaviour depends on the host page's HTML. Quick wins:
- Wrap content in
<main>or<article>. Anything outside is silently skipped. - Use real
<h1>–<h6>, not styled<div>s. Headings get a longer pause, which helps listeners orient. <a>nested in<p>/<li>/<h*>is handled smartly —"Link: "is injected inline without duplicating the text.<button>in the same position still reads twice — avoid it.- Form labels aren't read. Describe forms in a sibling
<p>if listeners need them.
See Architecture for the full picture of what gets read and in what order.
Verification checklist
After wiring it up:
- Network:
script.jsreturns 200 from the URL you expect. - DOM: a
<div class="webreader-panel">lives at the end of<body>. - Console: filter by
WebReader— no errors. - Play test: a homepage, an article, a form, a list view.
- Locale test: flip the language switcher, reopen the panel — UI strings should match.
Dynamic locale changes
The widget reads window.WebReaderConfig.locale once on init. The React example above re-runs its effect on i18n.language, which works because the new <script> tag overwrites the existing one. If you want an explicit tear-down + re-mount instead:
useEffect(() => {
document.getElementById("webreader-cdn-script")?.remove();
document.querySelector(".webreader-panel")?.remove();
(window as any).WebReaderConfig = { locale: currentLocale };
// ...append new script tag
}, [currentLocale]);
Most users pick a language and stick with it, so the cheaper "mount once" version is usually enough.
CORS
No extra setup needed. The widget calls https://synthesis.abair.ie directly, and the TTS API already allows cross-origin.
Self-hosting
If you ever need to host the widget at a different domain, build it as described in Development → Embed Server and serve script.js from any static host. Also serve the help and privacy pages so the in-widget help link still works.
Common gotchas
| Symptom | Cause / fix |
|---|---|
| CSP blocks the script | Add https://webreader.abair.ie to script-src, https://synthesis.abair.ie to connect-src and media-src. |
| Reads header/footer | Missing <main> — controller falls back to <body>. |
| Widget appears twice | Duplicate <script> tag (often from hot reload). The scriptId guard prevents it. |
| Nothing audible | Check DevTools → Network for synthesise requests. The widget falls back to browser speech synthesis if the TTS API is unreachable. |